"""
Extract the source of a trigger from an Object server.

  jython extract_trigger_src.py <object server> <trigger name>

Make sure you have jconn3.jar (from an Object server that has
nco_confpack) on your CLASSPATH.
"""
# If you don't connect to the object server using jconn3 you only get
# the first 255 chars of the trigger's codeblock

import re
import sys
import copy
from netcool import ObjectServerDB

class DB(object):
    """
    Handles extracting trigger details from an Object Server
    """
    def __init__(self, os):
        """
        Connect to an objectserver (pass it's hostname)
        """
        self.conn = ObjectServerDB.connect(os)

    def execute(self, sql, params=()):
        curs = self.conn.cursor()
        curs.execute(sql, params)
        return curs

    def details(self, trig_name, table_name):
        curs = self.execute("select * from %s where TriggerName = ?" % table_name, (trig_name, ))
        res = curs.fetchall()
        if len(res) > 1:
            raise ValueError("More than one trigger called %s in %s" % (trig_name, table_name))
        if len(res) <= 0:
            raise ValueError("No trigger called %s in %s" % (trig_name, table_name))
        
        fields = [d[0] for d in curs.description]
        return zip(fields, res[0])

    def basic(self, trig_name):
        "Attribs common to all triggers"
        return self.details(trig_name, "catalog.triggers")
        
    def database(self, trig_name):
        return self.details(trig_name, "catalog.database_triggers")
    
    def signal(self, trig_name):
        return self.details(trig_name, "catalog.signal_triggers")

    def temporal(self, trig_name):
        return self.details(trig_name, "catalog.temporal_triggers")

    def further(self, basic):
        "Attribs specific to a particular trigger type"
        dispatch = [self.database, self.signal, self.temporal]
        details = dict(basic)
        triggerkind = details["TriggerKind"]
        triggername = details["TriggerName"]
        return basic + dispatch[triggerkind](triggername)

    def trigger_details(self, trig_name):
        "All trigger details as a list of (Attrib, Value) tuples"
        return self.further(self.basic(trig_name))

    def list_triggers(self):
        sql = "select TriggerName from catalog.triggers"
        curs = self.execute(sql)
        return [r[0] for r in curs.fetchall()]


def enum(vals):
    "Maps database enumerations back to names"
    values = vals.split()
    return lambda x: values[x]


FILTERS = dict(
    compress = lambda s: re.sub(r"\n+", "\n", s),
    tf = enum("false true"),
    eventorder = enum("before after"),
    eventop = enum("insert reinsert update delete"),
    eventlevel = enum("row statement"))

def resolve(name, context):
    """
    Resolve a variable (in a code template) into a value.

    Tries to look the name up as an attribute of the context
    object. If the attrib is callable, it is called and should return
    a string which is interpreted as another template and evaluated in
    place. If the attrib is not callable, it's run through filters and
    returned.
    """
    name, filter = name.strip(), None
    if "|" in name:
        name, filter = [n.strip() for n in name.split("|", 1)]

    attr = getattr(context, name)

    # if the attr is callable, it will return another template to be
    # evaluated (there will be no filters)
    try:
        attr = render(attr(), context)
        return attr
    except TypeError:
        # so it's not callable 
        if filter:
            attr = FILTERS[filter](attr)
        return [str(attr)]
    

# simple templates, see http://effbot.org/zone/django-simple-template.htm
def render(template, context):
    """
    Parse template and resolve variables to values from context.
    """
    next = iter(re.split(r"({|})", template)).next
    data = []
    try:
        token = next()
        while 1:
            if token == "{":
                data.extend(resolve(next(), context))
                if next() != "}":
                    raise SyntaxError("Missing variable terminator")
            else:
                data.append(token)
            token = next()
    except StopIteration:
        pass
    return data
                            
    

class Trigger(object):
    """
    Abstract class for basic trigger details
    """
    def __init__(self, details):
        self.__dict__.update(dict(details))

    def when_block(self):
        if self.ConditionBlock:
            return "\nWHEN {ConditionBlock}"
        return ""

    def trigger_block(self):
        raise TypeError("Trigger is abstract")

    def declare_block(self):
        if self.DeclareBlock:
            if self.DeclareBlock.count("\n") == 0:
                return "\n" + "declare {DeclareBlock}"
            else:
                return "\n" + "declare\n{DeclareBlock}"
        return ""

    def debug_block(self):
        if self.DebugEnabled:
            return "\ndebug {DebugEnabled | tf}"
        return ""

    def enabled_block(self):
        if not self.IsEnabled:
            return "\nenabled {IsEnabled | tf}"
        return ""

    def code(self):
        # note that trigger_block is on it's own line, so the methods
        # don't need a leading newline
        template = """
create or replace trigger {TriggerName}
group {GroupName} {debug_block}{enabled_block}
priority {TriggerPriority}
comment '{CommentBlock}'
{trigger_block}{when_block}{declare_block}
begin
   {CodeBlock | compress}
end;
"""
        return render(template, self)



class DatabaseTrigger(Trigger):
    def __repr__(self):
        return "<DatabaseTrigger: %s>" % self.TriggerName

    def trigger_block(self):
        return """
{EventOrder | eventorder} {EventOp | eventop} on {DatabaseName}.{TableName}
for each {EventLevel | eventlevel}
""".strip()


class BindTrigger(Trigger):
    """
    Abstract class for triggers that bind statements.

    Temporal and Signal triggers allow
    EVALUATE select_cmd BIND AS variable name
    """
    
    def evaluate_block(self):
        if self.BindName:
            return "\n" + "evaluate {EvaluateBlock} bind as {BindName}"
        return ""


class SignalTrigger(BindTrigger):
    def __repr__(self):
        return "<SignalTrigger: %s>" % self.TriggerName

    def trigger_block(self):
        return "on signal {SignalName} {evaluate_block}"


class TemporalTrigger(BindTrigger):
    def __repr__(self):
        return "<TemporalTrigger: %s>" % self.TriggerName

    def trigger_block(self):
        # syntax allows hours, minutes or seconds but only seconds are stored
        freq = self.Frequency
        if freq % 3600 == 0:
            return "every %u hours {evaluate_block}" % (freq / 3600)
        elif freq % 60 == 0:
            return "every %u minutes {evaluate_block}""" % (freq / 60)
        return """every {Frequency} seconds {evaluate_block}"""



def make_trigger(details):
    "trigger factory"
    dispatch = [DatabaseTrigger, SignalTrigger, TemporalTrigger]
    triggerkind = [v for k,v in details if k == "TriggerKind"][0]
    return dispatch[triggerkind](details)

def list_triggers(objserver):
    db = DB(objserver)
    print "\n".join(db.list_triggers())

def trigger_code(objserver, trigger):
    db = DB(objserver)
    details =  db.trigger_details(trigger)
    trig = make_trigger(details)
    return trig.code()
    

if __name__ == "__main__":
    list = False
    if "-l" in sys.argv:
        sys.argv.remove("-l")
        list = True

    objectserver = sys.argv[1]
    if list:
        list_triggers(objectserver)
    else:
        trigger = sys.argv[2]
        sys.stdout.writelines(trigger_code(objectserver, trigger))


    # template test
#     template = "A {TriggerName} B {EventOrder | eventorder} C {trigger_block} D"
#     sys.stdout.writelines(render(template, trig))
